// Copyright (C) 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gles

import (
	"context"
	"fmt"
	"reflect"
	"time"

	"github.com/google/gapid/core/app/status"
	"github.com/google/gapid/core/data/compare"
	"github.com/google/gapid/core/log"
	"github.com/google/gapid/core/math/interval"
	"github.com/google/gapid/core/memory/arena"
	"github.com/google/gapid/gapis/api"
	"github.com/google/gapid/gapis/config"
	"github.com/google/gapid/gapis/memory"
	"github.com/google/gapid/gapis/replay/value"
)

const (
	maxDiffsLogged = 20
)

type stateBuilder struct {
	oldState        *api.GlobalState // The initial state which we are trying to recreate
	newState        *api.GlobalState // The recreated state generated by the commands
	cmds            []api.Cmd        // The commands which recreate the initial state
	cb              CommandBuilder   // Default command builder for thread 0
	preCmd          []func(api.Cmd)  // Actions to be done on the next written command
	tmpArena        arena.Arena      // The arena to use for temporary allocations
	seen            map[interface{}]bool
	memoryIntervals interval.U64RangeList
	cloneCtx        api.CloneContext
}

// RebuildState returns a set of commands which, if executed on a new clean
// state, will reproduce the API's state in s.
// The segments of memory that were used to create these commands are returned
// in the rangeList.
func (API) RebuildState(ctx context.Context, oldState *api.GlobalState) ([]api.Cmd, interval.U64RangeList) {
	start := time.Now()
	s, hasState := oldState.APIs[ID].(*State)
	if !hasState {
		return nil, nil
	}

	newState := api.NewStateWithAllocator(memory.NewBasicAllocator(value.ValidMemoryRanges), oldState.MemoryLayout)
	sb := &stateBuilder{
		oldState:        oldState,
		newState:        newState,
		cb:              CommandBuilder{Thread: 0, Arena: newState.Arena},
		tmpArena:        arena.New(),
		seen:            map[interface{}]bool{},
		memoryIntervals: interval.U64RangeList{},
		cloneCtx:        api.CloneContext{},
	}

	defer sb.tmpArena.Dispose()

	// Ensure that all pool IDs are distinct between the old state and new state.
	// This helps with verification at the end by ensuring that diffing algorithm
	// will not miss diffs just because the pool IDs happen to be same by chance.
	sb.newState.Memory.NewAt(sb.oldState.Memory.NextPoolID())

	// Create EGL contexts (possibly shared)
	representative := map[ShareListʳ]EGLContext{}
	for i := ContextID(0); i < s.NextContextID(); i++ {
		for handle, c := range s.EGLContexts().All() {
			// Don't recreate destroyed or uninitialized contexts.
			if c.Other().Destroyed() || !c.Other().Initialized() {
				continue
			}

			// TODO: We need to restore contexts in order without gaps, but this is messy.
			if c.Identifier() == i {
				sb.contextObject(ctx, handle, c, representative)
			}
		}
	}

	// Create EGL images (may depend on texture from any context)
	for _, img := range s.EGLImages().All() {
		sb.eglImage(ctx, img)
	}

	// Second pass over context which sets everything that may depend on EGLimage.
	for handle, c := range s.EGLContexts().All() {
		sb.contextObjectPostEGLImage(ctx, handle, c)
	}

	// Set the active context for each thread
	sb.bindContexts(ctx, s)

	log.I(ctx, "State reconstruction took %v", time.Since(start))

	if config.CheckRebuiltStateMatches {
		// Verify that the recreated state matches the original desired state.
		diffs := 0
		compare.Compare(s, GetState(sb.newState), func(d compare.Path) {
			if l := len(d); l > 1 {
				last := d[l-2] // l-1: is the pool, l-2 is the memory.Slice.
				if oldSlice, ok := last.Reference.(memory.Slice); ok {
					if newSlice, ok := last.Value.(memory.Slice); ok {
						old := AsU8ˢ(sb.tmpArena, oldSlice, sb.oldState.MemoryLayout)
						new := AsU8ˢ(sb.tmpArena, newSlice, sb.newState.MemoryLayout)
						if old.ResourceID(ctx, sb.oldState) == new.ResourceID(ctx, sb.newState) {
							return // The pool IDs are different, but the resource IDs match exactly.
						}
						oldData := old.MustRead(ctx, nil, sb.oldState, nil)
						newData := new.MustRead(ctx, nil, sb.newState, nil)
						if reflect.DeepEqual(oldData, newData) {
							return // The pool IDs are different, but the actual data matches exactly.
						}
					}
				}
			}
			if diffs++; diffs <= maxDiffsLogged {
				log.W(ctx, "Initial state: %v", d)
			}
		})

		if diffs > maxDiffsLogged {
			log.W(ctx, "Initial state: found an additional %d differences", diffs-maxDiffsLogged)
		}
	}

	return sb.cmds, sb.memoryIntervals
}

func (sb *stateBuilder) E(ctx context.Context, fmt string, args ...interface{}) {
	log.E(ctx, "[GL state builder] "+fmt, args...)
}

func (sb *stateBuilder) readsData(ctx context.Context, v interface{}) memory.Pointer {
	tmp := sb.newState.AllocDataOrPanic(ctx, v)
	rng := tmp.Range()
	interval.Merge(&sb.memoryIntervals, interval.U64Span{rng.Base, rng.Base + rng.Size}, true)
	sb.preCmd = append(sb.preCmd, func(cmd api.Cmd) {
		cmd.Extras().GetOrAppendObservations().AddRead(tmp.Data())
		tmp.Free()
	})
	return tmp.Ptr()
}

func (sb *stateBuilder) readsSlice(ctx context.Context, v U8ˢ) memory.Pointer {
	tmp := sb.newState.AllocOrPanic(ctx, v.Size())
	rng := tmp.Range()
	interval.Merge(&sb.memoryIntervals, interval.U64Span{rng.Base, rng.Base + rng.Size}, true)
	id := v.ResourceID(ctx, sb.oldState)
	sb.preCmd = append(sb.preCmd, func(cmd api.Cmd) {
		cmd.Extras().GetOrAppendObservations().AddRead(tmp.Range(), id)
		tmp.Free()
	})
	return tmp.Ptr()
}

func (sb *stateBuilder) writesData(ctx context.Context, v interface{}) memory.Pointer {
	tmp := sb.newState.AllocDataOrPanic(ctx, v)
	rng := tmp.Range()
	interval.Merge(&sb.memoryIntervals, interval.U64Span{rng.Base, rng.Base + rng.Size}, true)
	sb.preCmd = append(sb.preCmd, func(cmd api.Cmd) {
		cmd.Extras().GetOrAppendObservations().AddWrite(tmp.Data())
		tmp.Free()
	})
	return tmp.Ptr()
}

func (sb *stateBuilder) write(ctx context.Context, cmd api.Cmd) {
	for _, fn := range sb.preCmd {
		fn(cmd)
	}
	sb.preCmd = sb.preCmd[:0]
	if config.CheckRebuiltStateMatches {
		if err := cmd.Mutate(ctx, api.CmdNoID, sb.newState, nil, nil); err != nil {
			log.W(ctx, "Initial cmd %v: %v - %v", len(sb.cmds), cmd, err)
		}
	}
	sb.cmds = append(sb.cmds, cmd)
}

func (sb *stateBuilder) enable(ctx context.Context, cap GLenum, val GLboolean) {
	write, cb := sb.write, sb.cb
	if val == GLboolean_GL_TRUE {
		write(ctx, cb.GlEnable(cap))
	} else {
		write(ctx, cb.GlDisable(cap))
	}
}

func (sb *stateBuilder) enablei(ctx context.Context, cap GLenum, idx GLuint, val GLboolean) {
	write, cb := sb.write, sb.cb
	if val == GLboolean_GL_TRUE {
		write(ctx, cb.GlEnablei(cap, idx))
	} else {
		write(ctx, cb.GlDisablei(cap, idx))
	}
}

// once is used to ensure we create shared objects only once
func (sb *stateBuilder) once(key interface{}) (res bool) {
	res = !sb.seen[key]
	sb.seen[key] = true
	return
}

func (sb *stateBuilder) contextExtras(ctx context.Context, c Contextʳ) []api.CmdExtra {
	r := []api.CmdExtra{}
	if se := c.Other().StaticStateExtra(); !se.IsNil() {
		r = append(r, se.Get().Clone(sb.cb.Arena, sb.cloneCtx))
	}
	if de := c.Other().DynamicStateExtra(); !de.IsNil() {
		r = append(r, de.Get().Clone(sb.cb.Arena, sb.cloneCtx))
	}
	return r
}

func (sb *stateBuilder) contextObject(ctx context.Context, handle EGLContext, c Contextʳ, representative map[ShareListʳ]EGLContext) {
	ctx = status.Start(ctx, "contextObject %v", c.Identifier())
	defer status.Finish(ctx)

	write, cb := sb.write, sb.cb

	// Check if we have already created any other context within this share-list.
	sharedCtx := representative[c.Other().ShareList()] // Returns nullptr if we are the first.
	representative[c.Other().ShareList()] = handle     // Further contexts will reference us.

	// TODO: Record the arguments in state.
	write(ctx, cb.EglCreateContext(memory.Nullptr, memory.Nullptr, sharedCtx, memory.Nullptr, handle))
	write(ctx, api.WithExtras(cb.EglMakeCurrent(memory.Nullptr, memory.Nullptr, memory.Nullptr, handle, EGLBoolean(1)),
		sb.contextExtras(ctx, c)...))

	write(ctx, cb.GlPixelStorei(GLenum_GL_UNPACK_ALIGNMENT, 1))

	if names := c.Objects().GeneratedNames().Buffers(); sb.once(names) && names.Len() > 0 {
		status.Do(ctx, fmt.Sprintf("rebuilding %d buffers", names.Len()), func(ctx context.Context) {
			for _, id := range names.Keys() {
				if id != 0 && names.Get(id) {
					write(ctx, cb.GlGenBuffers(1, sb.writesData(ctx, id)))
					if o := c.Objects().Buffers().Get(id); !o.IsNil() {
						sb.bufferObject(ctx, o)
					}
				}
			}
		})
	}
	if names := c.Objects().GeneratedNames().Renderbuffers(); sb.once(names) && names.Len() > 0 {
		status.Do(ctx, fmt.Sprintf("rebuilding %d renderbuffers", names.Len()), func(ctx context.Context) {
			for _, id := range names.Keys() {
				if id != 0 && names.Get(id) {
					write(ctx, cb.GlGenRenderbuffers(1, sb.writesData(ctx, id)))
					if o := c.Objects().Renderbuffers().Get(id); !o.IsNil() {
						sb.renderbufferObject(ctx, o)
					}
				}
			}
		})
	}
	for _, defaultTexture := range []Textureʳ{
		c.Objects().Default().Texture2d(),
		c.Objects().Default().Texture2dArray(),
		c.Objects().Default().Texture2dMultisample(),
		c.Objects().Default().Texture2dMultisampleArray(),
		c.Objects().Default().Texture3d(),
		c.Objects().Default().TextureBuffer(),
		c.Objects().Default().TextureCubeMap(),
		c.Objects().Default().TextureCubeMapArray(),
		c.Objects().Default().TextureExternalOes(),
	} {
		sb.textureObject(ctx, defaultTexture)
	}
	if names := c.Objects().GeneratedNames().Textures(); sb.once(names) && names.Len() > 0 {
		status.Do(ctx, fmt.Sprintf("rebuilding %d textures", names.Len()), func(ctx context.Context) {
			for _, id := range names.Keys() {
				if id != 0 && names.Get(id) {
					write(ctx, cb.GlGenTextures(1, sb.writesData(ctx, id)))
					if o := c.Objects().Textures().Get(id); !o.IsNil() {
						sb.textureObject(ctx, o)
					}
				}
			}
		})
	}
	if objs := c.Objects().ImageUnits(); sb.once(objs) && objs.Len() > 0 {
		status.Do(ctx, fmt.Sprintf("rebuilding %d image units", objs.Len()), func(ctx context.Context) {
			for _, id := range objs.Keys() {
				if o := c.Objects().ImageUnits().Get(id); !o.IsNil() {
					sb.imageUnit(ctx, o)
				}
			}
		})
	}
	var programs []ProgramId
	if objs := c.Objects().Programs(); sb.once(objs) && objs.Len() > 0 {
		status.Do(ctx, fmt.Sprintf("rebuilding %d programs", objs.Len()), func(ctx context.Context) {
			for _, id := range objs.Keys() {
				if o := c.Objects().Programs().Get(id); !o.IsNil() {
					sb.programObject(ctx, o)
					programs = append(programs, id)
				}
			}
		})
	}
	if objs := c.Objects().Shaders(); sb.once(objs) && objs.Len() > 0 {
		status.Do(ctx, fmt.Sprintf("rebuilding %d shaders", objs.Len()), func(ctx context.Context) {
			for _, id := range objs.Keys() {
				if o := c.Objects().Shaders().Get(id); !o.IsNil() {
					sb.shaderObject(ctx, o)
				}
			}
		})
	}
	if len(programs) > 0 {
		status.Do(ctx, fmt.Sprintf("attaching shaders to %d programs", len(programs)), func(ctx context.Context) {
			for _, id := range programs {
				sb.attachShaders(ctx, c.Objects().Programs().Get(id))
			}
		})
	}
	if names := c.Objects().GeneratedNames().Pipelines(); sb.once(names) && names.Len() > 0 {
		status.Do(ctx, fmt.Sprintf("rebuilding %d pipelines", names.Len()), func(ctx context.Context) {
			for _, id := range names.Keys() {
				if id != 0 && names.Get(id) {
					write(ctx, cb.GlGenProgramPipelines(1, sb.writesData(ctx, id)))
					if o := c.Objects().Pipelines().Get(id); !o.IsNil() {
						sb.pipelineObject(ctx, o)
					}
				}
			}
		})
	}
	if names := c.Objects().GeneratedNames().Samplers(); sb.once(names) && names.Len() > 0 {
		status.Do(ctx, fmt.Sprintf("rebuilding %d samplers", names.Len()), func(ctx context.Context) {
			for _, id := range names.Keys() {
				if id != 0 && names.Get(id) {
					write(ctx, cb.GlGenSamplers(1, sb.writesData(ctx, id)))
					if o := c.Objects().Samplers().Get(id); !o.IsNil() {
						sb.samplerObject(ctx, o)
					}
				}
			}
		})
	}
	if names := c.Objects().GeneratedNames().Queries(); sb.once(names) && names.Len() > 0 {
		status.Do(ctx, fmt.Sprintf("rebuilding %d queries", names.Len()), func(ctx context.Context) {
			for _, id := range names.Keys() {
				if id != 0 && names.Get(id) {
					write(ctx, cb.GlGenQueries(1, sb.writesData(ctx, id)))
					if o := c.Objects().Queries().Get(id); !o.IsNil() {
						sb.queryObject(ctx, o)
					}
				}
			}
		})
	}
	if objs := c.Objects().SyncObjects(); sb.once(objs) && objs.Len() > 0 {
		status.Do(ctx, fmt.Sprintf("rebuilding %d sync objects", objs.Len()), func(ctx context.Context) {
			for _, id := range objs.Keys() {
				if o := c.Objects().SyncObjects().Get(id); !o.IsNil() {
					sb.syncObject(ctx, o)
				}
			}
		})
	}
	sb.transformFeedbackObject(ctx, c.Objects().Default().TransformFeedback())
	if names := c.Objects().GeneratedNames().TransformFeedbacks(); sb.once(names) && names.Len() > 0 {
		status.Do(ctx, fmt.Sprintf("rebuilding %d tf feedback objects", names.Len()), func(ctx context.Context) {
			for _, id := range names.Keys() {
				if id != 0 && names.Get(id) {
					write(ctx, cb.GlGenTransformFeedbacks(1, sb.writesData(ctx, id)))
					if o := c.Objects().TransformFeedbacks().Get(id); !o.IsNil() {
						sb.transformFeedbackObject(ctx, o)
					}
				}
			}
		})
	}
	sb.vertexArrayObject(ctx, c.Objects().Default().VertexArray())
	if names := c.Objects().GeneratedNames().VertexArrays(); sb.once(names) && names.Len() > 0 {
		status.Do(ctx, fmt.Sprintf("rebuilding %d VAOs", names.Len()), func(ctx context.Context) {
			for _, id := range names.Keys() {
				if id != 0 && names.Get(id) {
					write(ctx, cb.GlGenVertexArrays(1, sb.writesData(ctx, id)))
					if o := c.Objects().VertexArrays().Get(id); !o.IsNil() {
						sb.vertexArrayObject(ctx, o)
					}
				}
			}
		})
	}
	sb.vertexState(ctx, c.Vertex())
	sb.reasterizationState(ctx, c.Rasterization())
	sb.pixelState(ctx, c.Pixel())
	sb.otherState(ctx, c.Other())
	sb.debugLabels(ctx, c)
	sb.bindings(ctx, c)
	sb.deleteMarkedObjects(ctx, c)

	write(ctx, cb.EglMakeCurrent(memory.Nullptr, memory.Nullptr, memory.Nullptr, memory.Nullptr, EGLBoolean(1)))
}

// eglImage creates an EGLImage object.
// It may reference a texture object in different context,
// so it must be called after all contexts have been created.
func (sb *stateBuilder) eglImage(ctx context.Context, img EGLImageʳ) {
	write, cb := sb.write, sb.cb

	// TODO: This might not work if the target texture object has been deleted.
	attribs := img.AttribList().MustRead(ctx, nil, sb.oldState, nil)
	cmd := cb.EglCreateImageKHR(img.Display(), img.Context(), img.Target(), img.Buffer(), sb.readsData(ctx, attribs), img.ID())
	if extra := img.Extra(); !extra.IsNil() {
		cmd.Extras().Add(extra.Get().Clone(cb.Arena, sb.cloneCtx))
	}
	write(ctx, cmd)
}

func (sb *stateBuilder) contextObjectPostEGLImage(ctx context.Context, handle EGLContext, c Contextʳ) {
	write, cb := sb.write, sb.cb

	if c.Other().Initialized() {
		write(ctx, api.WithExtras(cb.EglMakeCurrent(memory.Nullptr, memory.Nullptr, memory.Nullptr, handle, EGLBoolean(1)),
			sb.contextExtras(ctx, c)...))

		for _, t := range c.Objects().Textures().All() {
			target := t.Kind()
			if i := t.EGLImage(); !i.IsNil() {
				write(ctx, cb.GlBindTexture(target, t.GetID()))
				if target != GLenum_GL_TEXTURE_2D && target != GLenum_GL_TEXTURE_EXTERNAL_OES {
					panic(fmt.Errorf("Unknown EGLImage target: %v", target))
				}
				img := t.Image()
				extra := &EGLImageData{
					ID:     img.Data().ResourceID(ctx, sb.oldState),
					Size:   img.Data().Size(),
					Width:  img.Width(),
					Height: img.Height(),
					Format: img.DataFormat(),
					Type:   img.DataType(),
				}
				write(ctx, api.WithExtras(cb.GlEGLImageTargetTexture2DOES(target, i.ID()), extra))
			}
		}

		// Create framebuffers
		sb.framebufferObject(ctx, c, c.Objects().Default().Framebuffer())
		if names := c.Objects().GeneratedNames().Framebuffers(); sb.once(names) {
			for _, id := range names.Keys() {
				if id != 0 && names.Get(id) {
					write(ctx, cb.GlGenFramebuffers(1, sb.writesData(ctx, id)))
					if o := c.Objects().Framebuffers().Get(id); !o.IsNil() {
						sb.framebufferObject(ctx, c, o)
					}
				}
			}
		}

		// Framebuffer bindings
		write(ctx, cb.GlBindFramebuffer(GLenum_GL_READ_FRAMEBUFFER, c.Bound().ReadFramebuffer().GetID()))
		write(ctx, cb.GlBindFramebuffer(GLenum_GL_DRAW_FRAMEBUFFER, c.Bound().DrawFramebuffer().GetID()))
		write(ctx, cb.GlBindRenderbuffer(GLenum_GL_RENDERBUFFER, c.Bound().Renderbuffer().GetID()))

		// Texture unit bindings
		for unit, tu := range c.Objects().TextureUnits().All() {
			bind := func(target GLenum, tex Textureʳ) {
				if t := tex.GetID(); t != 0 || unit == 0 {
					write(ctx, cb.GlActiveTexture(GLenum_GL_TEXTURE0+GLenum(unit)))
					write(ctx, cb.GlBindTexture(target, t))
				}
			}
			bind(GLenum_GL_TEXTURE_2D, tu.Binding2d())
			bind(GLenum_GL_TEXTURE_2D_ARRAY, tu.Binding2dArray())
			bind(GLenum_GL_TEXTURE_2D_MULTISAMPLE, tu.Binding2dMultisample())
			bind(GLenum_GL_TEXTURE_2D_MULTISAMPLE_ARRAY, tu.Binding2dMultisampleArray())
			bind(GLenum_GL_TEXTURE_3D, tu.Binding3d())
			bind(GLenum_GL_TEXTURE_BUFFER, tu.BindingBuffer())
			bind(GLenum_GL_TEXTURE_CUBE_MAP, tu.BindingCubeMap())
			bind(GLenum_GL_TEXTURE_CUBE_MAP_ARRAY, tu.BindingCubeMapArray())
			bind(GLenum_GL_TEXTURE_EXTERNAL_OES, tu.BindingExternalOes())
		}
		write(ctx, cb.GlActiveTexture(GLenum_GL_TEXTURE0+GLenum(c.Bound().TextureUnit().ID())))

		write(ctx, cb.EglMakeCurrent(memory.Nullptr, memory.Nullptr, memory.Nullptr, memory.Nullptr, EGLBoolean(1)))
	}
}

// We have generally setup the whole state on single-thread.
// As a last step, call eglMakeCurrent for on all user-threads.
func (sb *stateBuilder) bindContexts(ctx context.Context, s *State) {
	write, cb := sb.write, sb.cb

	for handle, c := range s.EGLContexts().All() {
		if thread := c.Other().BoundOnThread(); thread != 0 {
			cb := CommandBuilder{Thread: thread, Arena: sb.cb.Arena}
			write(ctx, api.WithExtras(cb.EglMakeCurrent(memory.Nullptr, memory.Nullptr, memory.Nullptr, handle, EGLBoolean(1)),
				sb.contextExtras(ctx, c)...))
		}
	}
	write(ctx, cb.EglMakeCurrent(memory.Nullptr, memory.Nullptr, memory.Nullptr, memory.Nullptr, EGLBoolean(1)))
}

func (sb *stateBuilder) bufferObject(ctx context.Context, b Bufferʳ) {
	write, cb, id := sb.write, sb.cb, b.GetID()
	target := GLenum_GL_ARRAY_BUFFER // Any binding point will do.

	write(ctx, cb.GlBindBuffer(target, id))
	write(ctx, cb.GlBufferData(target, GLsizeiptr(b.Data().Size()), sb.readsSlice(ctx, b.Data()), b.Usage()))
	if b.Mapped() == GLboolean_GL_TRUE {
		write(ctx, cb.GlMapBufferRange(target, b.MapOffset(), b.MapLength(), b.AccessFlags(), b.MapPointer())) // GLES30
	}
}

// Just to be on the safe side, this should be done before textures and framebuffers,
// since we generate some of them as temporary objects (to avoid ID collisions).
func (sb *stateBuilder) renderbufferObject(ctx context.Context, rb Renderbufferʳ) {
	write, cb, id := sb.write, sb.cb, rb.GetID()

	write(ctx, cb.GlBindRenderbuffer(GLenum_GL_RENDERBUFFER, id))

	if img := rb.Image(); !img.IsNil() {
		fmt, w, h := img.SizedFormat(), img.Width(), img.Height()
		write(ctx, cb.GlRenderbufferStorageMultisample(GLenum_GL_RENDERBUFFER, img.Samples(), fmt, w, h)) // GLES30

		// Fill the renderbuffer with data using a framebuffer blit
		if img.Data().Size() != 0 {
			// Create temporary objects
			tex := TextureId(0x10000001)
			write(ctx, cb.GlGenTextures(1, sb.writesData(ctx, tex)))
			write(ctx, cb.GlBindTexture(GLenum_GL_TEXTURE_2D, tex))
			src := FramebufferId(0x10000002)
			write(ctx, cb.GlGenFramebuffers(1, sb.writesData(ctx, src)))
			write(ctx, cb.GlBindFramebuffer(GLenum_GL_READ_FRAMEBUFFER, src))
			dst := FramebufferId(0x10000003)
			write(ctx, cb.GlGenFramebuffers(1, sb.writesData(ctx, dst)))
			write(ctx, cb.GlBindFramebuffer(GLenum_GL_DRAW_FRAMEBUFFER, dst))

			// Upload data and blit it to our renderbuffer
			att := GLenum_GL_COLOR_ATTACHMENT0 // TODO: Consider depth
			write(ctx, cb.GlTexImage2D(GLenum_GL_TEXTURE_2D, 0, GLint(fmt), w, h, 0, img.DataFormat(), img.DataType(), sb.readsSlice(ctx, img.Data())))
			write(ctx, cb.GlFramebufferTexture2D(GLenum_GL_READ_FRAMEBUFFER, att, GLenum_GL_TEXTURE_2D, tex, 0))
			write(ctx, cb.GlFramebufferRenderbuffer(GLenum_GL_DRAW_FRAMEBUFFER, att, GLenum_GL_RENDERBUFFER, id))
			write(ctx, cb.GlScissor(0, 0, w, h))
			mask := GLbitfield_GL_COLOR_BUFFER_BIT | GLbitfield_GL_DEPTH_BUFFER_BIT | GLbitfield_GL_STENCIL_BUFFER_BIT
			write(ctx, cb.GlBlitFramebuffer(0, 0, GLint(w), GLint(h), 0, 0, GLint(w), GLint(h), mask, GLenum_GL_NEAREST))

			// Delete temporary objects
			write(ctx, cb.GlBindTexture(GLenum_GL_TEXTURE_2D, 0))
			write(ctx, cb.GlDeleteTextures(1, sb.readsData(ctx, tex)))
			write(ctx, cb.GlBindFramebuffer(GLenum_GL_READ_FRAMEBUFFER, 0))
			write(ctx, cb.GlDeleteFramebuffers(1, sb.readsData(ctx, src)))
			write(ctx, cb.GlBindFramebuffer(GLenum_GL_DRAW_FRAMEBUFFER, 0))
			write(ctx, cb.GlDeleteFramebuffers(1, sb.readsData(ctx, dst)))
		}
	}
}

func getTextureTargetInfo(target GLenum) (isMultisample, isArray, is3D bool) {
	switch target {
	case GLenum_GL_TEXTURE_2D:
		break
	case GLenum_GL_TEXTURE_2D_ARRAY:
		isArray = true
	case GLenum_GL_TEXTURE_2D_MULTISAMPLE:
		isMultisample = true
	case GLenum_GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
		isMultisample, isArray = true, true
	case GLenum_GL_TEXTURE_3D:
		is3D = true
	case GLenum_GL_TEXTURE_BUFFER:
		break
	case GLenum_GL_TEXTURE_CUBE_MAP:
		break
	case GLenum_GL_TEXTURE_CUBE_MAP_ARRAY:
		isArray = true
	case GLenum_GL_TEXTURE_EXTERNAL_OES:
		break
	default:
		panic(fmt.Errorf("Unsupported texture type: %v", target))
	}
	return
}

// must be after renderbuffers and textures
func (sb *stateBuilder) framebufferObject(ctx context.Context, c Contextʳ, fb Framebufferʳ) {
	write, cb, id := sb.write, sb.cb, fb.GetID()

	target := GLenum_GL_FRAMEBUFFER
	write(ctx, cb.GlBindFramebuffer(target, id))

	if id == 0 {
		if drawBuffer := fb.DrawBuffer().Get(0); drawBuffer != GLenum_GL_BACK {
			write(ctx, cb.GlDrawBuffers(1, sb.readsData(ctx, drawBuffer)))
		}
		if fb.ReadBuffer() != GLenum_GL_BACK {
			write(ctx, cb.GlReadBuffer(fb.ReadBuffer()))
		}
	} else {
		// Attachments
		attach := func(name GLenum, a FramebufferAttachment) {
			switch a.Type() {
			case GLenum_GL_RENDERBUFFER:
				write(ctx, cb.GlFramebufferRenderbuffer(target, name, a.Type(), a.Renderbuffer().GetID()))
			case GLenum_GL_TEXTURE:
				ty, id, level, layer := a.Texture().Kind(), a.Texture().GetID(), a.TextureLevel(), a.TextureLayer()
				_, isArray, is3D := getTextureTargetInfo(ty)
				if a.NumViews() > 1 {
					write(ctx, cb.GlFramebufferTextureMultiviewOVR(target, name, id, level, layer, a.NumViews()))
				} else if ty == GLenum_GL_TEXTURE_CUBE_MAP {
					if a.Layered() == GLboolean_GL_TRUE {
						write(ctx, cb.GlFramebufferTexture(target, name, id, level)) // GLES32
					} else {
						ty = GLenum_GL_TEXTURE_CUBE_MAP_POSITIVE_X + GLenum(a.TextureLayer()%6)
						write(ctx, cb.GlFramebufferTexture2D(target, name, ty, id, level))
					}
				} else if isArray || is3D {
					if a.Layered() == GLboolean_GL_TRUE {
						write(ctx, cb.GlFramebufferTexture(target, name, id, level)) // GLES32
					} else if a.NumViews() == 1 {
						write(ctx, cb.GlFramebufferTextureLayer(target, name, id, level, layer)) // GLES30
					}
				} else {
					write(ctx, cb.GlFramebufferTexture2D(target, name, ty, id, level))
				}
			}
		}
		for i, a := range fb.ColorAttachments().All() {
			attach(GLenum_GL_COLOR_ATTACHMENT0+GLenum(i), a)
		}
		attach(GLenum_GL_DEPTH_ATTACHMENT, fb.DepthAttachment())
		attach(GLenum_GL_STENCIL_ATTACHMENT, fb.StencilAttachment())

		// Active attachments
		drawBuffers := []GLenum{}
		for i := 0; i < fb.DrawBuffer().Len(); i++ {
			drawBuffers = append(drawBuffers, fb.DrawBuffer().Get(GLint(i)))
		}
		write(ctx, cb.GlDrawBuffers(GLsizei(len(drawBuffers)), sb.readsData(ctx, drawBuffers)))
		write(ctx, cb.GlReadBuffer(fb.ReadBuffer()))

		// Parameters
		param := func(name GLenum, value GLint) {
			write(ctx, cb.GlFramebufferParameteri(target, name, value))
		}
		param(GLenum_GL_FRAMEBUFFER_DEFAULT_WIDTH, fb.DefaultWidth())     // GLES31
		param(GLenum_GL_FRAMEBUFFER_DEFAULT_HEIGHT, fb.DefaultHeight())   // GLES31
		param(GLenum_GL_FRAMEBUFFER_DEFAULT_LAYERS, fb.DefaultLayers())   // GLES32
		param(GLenum_GL_FRAMEBUFFER_DEFAULT_SAMPLES, fb.DefaultSamples()) // GLES31
		if fb.DefaultFixedSampleLocations() == GLboolean_GL_TRUE {
			param(GLenum_GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS, 1) // GLES31
		}
	}
}

func (sb *stateBuilder) imageUnit(ctx context.Context, i ImageUnitʳ) {
	write, cb, id := sb.write, sb.cb, i.GetID()

	if !i.Texture().IsNil() {
		write(ctx, cb.GlBindImageTexture(id, i.Texture().GetID(), i.Level(), i.Layered(), i.Layer(), i.Access(), i.Fmt())) // GLES31
	}
}

func (sb *stateBuilder) shaderObject(ctx context.Context, s Shaderʳ) {
	write, cb, id := sb.write, sb.cb, s.GetID()

	write(ctx, cb.GlCreateShader(s.Type(), id))
	if e := s.CompileExtra(); !e.IsNil() {
		if !e.Binary().IsNil() {
			sb.E(ctx, "Precompiled shaders not suppored yet") // TODO
		}
		write(ctx, cb.GlShaderSource(id, 1, sb.readsData(ctx, sb.readsData(ctx, e.Source())), memory.Nullptr))
		write(ctx, api.WithExtras(cb.GlCompileShader(id), e.Get().Clone(cb.Arena, sb.cloneCtx)))
	}
	write(ctx, cb.GlShaderSource(id, 1, sb.readsData(ctx, sb.readsData(ctx, s.Source())), memory.Nullptr))
}

func (sb *stateBuilder) programObject(ctx context.Context, p Programʳ) {
	write, cb, id := sb.write, sb.cb, p.GetID()

	write(ctx, cb.GlCreateProgram(id))
	for name, location := range p.AttributeBindings().All() {
		write(ctx, cb.GlBindAttribLocation(id, location, sb.readsData(ctx, name)))
	}
	if count := p.TransformFeedbackVaryings().Len(); count > 0 {
		varyings := make([]memory.Pointer, count)
		for i, varying := range p.TransformFeedbackVaryings().All() {
			varyings[i] = sb.readsData(ctx, varying)
		}
		mode := p.TransformFeedbackBufferMode()
		write(ctx, cb.GlTransformFeedbackVaryings(id, GLsizei(len(varyings)), sb.readsData(ctx, varyings), mode))
	}
	if p.Separable() {
		write(ctx, cb.GlProgramParameteri(id, GLenum_GL_PROGRAM_SEPARABLE, 1))
	}
	if p.BinaryRetrievableHint() {
		write(ctx, cb.GlProgramParameteri(id, GLenum_GL_PROGRAM_BINARY_RETRIEVABLE_HINT, 1))
	}
	if !p.LinkExtra().IsNil() {
		if p.SuccessfulLinkExtra() != p.LinkExtra() {
			sb.E(ctx, "Stale program executable not suppored yet") // TODO
		}
		if !p.LinkExtra().Binary().IsNil() {
			sb.E(ctx, "Precompiled programs not suppored yet") // TODO
		}

		// Create the shaders from the extra.
		attachedShaders := []ShaderId{}
		for t, s := range p.LinkExtra().Shaders().All() {
			if !s.Binary().IsNil() {
				sb.E(ctx, "Precompiled programs not suppored yet") // TODO
				continue
			}
			write(ctx, cb.GlCreateShader(t, s.ID()))
			write(ctx, cb.GlShaderSource(s.ID(), 1, sb.readsData(ctx, sb.readsData(ctx, s.Source())), memory.Nullptr))
			write(ctx, api.WithExtras(cb.GlCompileShader(s.ID()), s.Get().Clone(cb.Arena, sb.cloneCtx)))
			write(ctx, cb.GlAttachShader(id, s.ID()))
			attachedShaders = append(attachedShaders, s.ID())
		}

		write(ctx, api.WithExtras(cb.GlLinkProgram(id), p.LinkExtra().Get().Clone(cb.Arena, sb.cloneCtx)))
		write(ctx, cb.GlUseProgram(id))
		for _, u := range p.ActiveResources().DefaultUniformBlock().All() {
			if loc, ok := u.Locations().Lookup(0); ok {
				sb.uniform(ctx, u.Type(), UniformLocation(loc), GLsizei(u.ArraySize()), sb.readsSlice(ctx, u.Value()))
			}
		}
		for loc, b := range p.ActiveResources().UniformBlocks().All() {
			if index := b.Binding(); index > 0 {
				write(ctx, cb.GlUniformBlockBinding(id, UniformBlockIndex(loc), GLuint(index)))
			}
		}
		write(ctx, cb.GlUseProgram(0))

		// Detach and delete the linked shaders (we'll attach the shaders from the state later).
		for _, shaderID := range attachedShaders {
			write(ctx, cb.GlDetachShader(id, shaderID))
			write(ctx, cb.GlDeleteShader(shaderID))
		}
	}
	if !p.ValidateExtra().IsNil() {
		write(ctx, api.WithExtras(cb.GlValidateProgram(id), p.ValidateExtra().Get().Clone(cb.Arena, sb.cloneCtx)))
	}
}

func (sb *stateBuilder) attachShaders(ctx context.Context, p Programʳ) {
	write, cb, id := sb.write, sb.cb, p.GetID()

	for _, s := range p.Shaders().All() {
		if s := s.GetID(); s != 0 {
			write(ctx, cb.GlAttachShader(id, s))
		}
	}
}

func (sb *stateBuilder) uniform(ctx context.Context, ty GLenum, loc UniformLocation, n GLsizei, v memory.Pointer) {
	write, cb := sb.write, sb.cb

	switch ty {
	case GLenum_GL_FLOAT:
		write(ctx, cb.GlUniform1fv(loc, n, v))
	case GLenum_GL_FLOAT_VEC2:
		write(ctx, cb.GlUniform2fv(loc, n, v))
	case GLenum_GL_FLOAT_VEC3:
		write(ctx, cb.GlUniform3fv(loc, n, v))
	case GLenum_GL_FLOAT_VEC4:
		write(ctx, cb.GlUniform4fv(loc, n, v))
	case GLenum_GL_BOOL:
	case GLenum_GL_INT:
		write(ctx, cb.GlUniform1iv(loc, n, v))
	case GLenum_GL_BOOL_VEC2:
	case GLenum_GL_INT_VEC2:
		write(ctx, cb.GlUniform2iv(loc, n, v))
	case GLenum_GL_BOOL_VEC3:
	case GLenum_GL_INT_VEC3:
		write(ctx, cb.GlUniform3iv(loc, n, v))
	case GLenum_GL_BOOL_VEC4:
	case GLenum_GL_INT_VEC4:
		write(ctx, cb.GlUniform4iv(loc, n, v))
	case GLenum_GL_UNSIGNED_INT:
		write(ctx, cb.GlUniform1uiv(loc, n, v))
	case GLenum_GL_UNSIGNED_INT_VEC2:
		write(ctx, cb.GlUniform2uiv(loc, n, v))
	case GLenum_GL_UNSIGNED_INT_VEC3:
		write(ctx, cb.GlUniform3uiv(loc, n, v))
	case GLenum_GL_UNSIGNED_INT_VEC4:
		write(ctx, cb.GlUniform4uiv(loc, n, v))
	case GLenum_GL_FLOAT_MAT2:
		write(ctx, cb.GlUniformMatrix2fv(loc, n, GLboolean_GL_FALSE, v))
	case GLenum_GL_FLOAT_MAT3:
		write(ctx, cb.GlUniformMatrix3fv(loc, n, GLboolean_GL_FALSE, v))
	case GLenum_GL_FLOAT_MAT4:
		write(ctx, cb.GlUniformMatrix4fv(loc, n, GLboolean_GL_FALSE, v))
	case GLenum_GL_FLOAT_MAT2x3:
		write(ctx, cb.GlUniformMatrix2x3fv(loc, n, GLboolean_GL_FALSE, v))
	case GLenum_GL_FLOAT_MAT2x4:
		write(ctx, cb.GlUniformMatrix2x4fv(loc, n, GLboolean_GL_FALSE, v))
	case GLenum_GL_FLOAT_MAT3x2:
		write(ctx, cb.GlUniformMatrix3x2fv(loc, n, GLboolean_GL_FALSE, v))
	case GLenum_GL_FLOAT_MAT3x4:
		write(ctx, cb.GlUniformMatrix3x4fv(loc, n, GLboolean_GL_FALSE, v))
	case GLenum_GL_FLOAT_MAT4x2:
		write(ctx, cb.GlUniformMatrix4x2fv(loc, n, GLboolean_GL_FALSE, v))
	case GLenum_GL_FLOAT_MAT4x3:
		write(ctx, cb.GlUniformMatrix4x3fv(loc, n, GLboolean_GL_FALSE, v))
	default:
		write(ctx, cb.GlUniform1iv(loc, n, v))
	}
}

func (sb *stateBuilder) pipelineObject(ctx context.Context, pipe Pipelineʳ) {
	write, cb, id := sb.write, sb.cb, pipe.GetID()

	write(ctx, cb.GlBindProgramPipeline(id))
	write(ctx, cb.GlActiveShaderProgram(id, pipe.ActiveProgram().GetID()))
	write(ctx, cb.GlUseProgramStages(id, GLbitfield_GL_COMPUTE_SHADER_BIT, pipe.ComputeShader().GetID()))
	write(ctx, cb.GlUseProgramStages(id, GLbitfield_GL_FRAGMENT_SHADER_BIT, pipe.FragmentShader().GetID()))
	write(ctx, cb.GlUseProgramStages(id, GLbitfield_GL_VERTEX_SHADER_BIT, pipe.VertexShader().GetID()))
	write(ctx, cb.GlUseProgramStages(id, GLbitfield_GL_TESS_CONTROL_SHADER_BIT, pipe.TessControlShader().GetID()))
	write(ctx, cb.GlUseProgramStages(id, GLbitfield_GL_TESS_EVALUATION_SHADER_BIT, pipe.TessEvaluationShader().GetID()))
	write(ctx, cb.GlUseProgramStages(id, GLbitfield_GL_GEOMETRY_SHADER_BIT, pipe.GeometryShader().GetID()))
	if !pipe.ValidateExtra().IsNil() {
		write(ctx, api.WithExtras(cb.GlValidateProgramPipeline(id), pipe.ValidateExtra().Get().Clone(cb.Arena, sb.cloneCtx)))
	}
	write(ctx, cb.GlBindProgramPipeline(0))
}

func (sb *stateBuilder) textureObject(ctx context.Context, t Textureʳ) {
	write, cb, id := sb.write, sb.cb, t.GetID()

	target := t.Kind()
	isMultisample, isArray, is3D := getTextureTargetInfo(target)
	isCompressed := func(img Imageʳ) bool {
		return GetSizedFormatInfoOrPanic(img.SizedFormat()).Compression() != CompressionAlgorithm_Uncompressed
	}

	write(ctx, cb.GlBindTexture(target, id))

	// Allocate space for texture data
	if target == GLenum_GL_TEXTURE_BUFFER {
		b := t.Buffer()
		write(ctx, cb.GlTexBufferRange(target, b.InternalFormat(), b.Binding().GetID(), b.Offset(), b.Size()))
	} else if target == GLenum_GL_TEXTURE_EXTERNAL_OES {
		// The dimensions are fully specified by the EGLimage
	} else if t.ImmutableFormat() == GLboolean_GL_TRUE {
		img := t.Levels().Get(0).Layers().Get(0) // Must exist
		lvl, fmt := GLsizei(t.Levels().Len()), internalFormat(img.SizedFormat())
		w, h, d := img.Width(), img.Height(), GLsizei(t.Levels().Get(0).Layers().Len())
		samples, fixed := img.Samples(), img.FixedSampleLocations()

		if isMultisample {
			if isArray || is3D {
				write(ctx, cb.GlTexStorage3DMultisample(target, samples, fmt, w, h, d, fixed))
			} else {
				write(ctx, cb.GlTexStorage2DMultisample(target, samples, fmt, w, h, fixed))
			}
		} else {
			if isArray || is3D {
				write(ctx, cb.GlTexStorage3D(target, lvl, fmt, w, h, d))
			} else {
				write(ctx, cb.GlTexStorage2D(target, lvl, fmt, w, h))
			}
		}
	} else if isArray || is3D {
		for lvl, levelObject := range t.Levels().All() {
			img := levelObject.Layers().Get(0) // Must exist, all layers must be consistent.
			fmt, w, h, d := internalFormat(img.SizedFormat()), img.Width(), img.Height(), GLsizei(levelObject.Layers().Len())
			dataFormat, dataType := img.getUnsizedFormatAndType()
			if isCompressed(img) {
				dataSize := GLsizei(img.Data().Size()) * d
				write(ctx, cb.GlCompressedTexImage3D(target, lvl, fmt, w, h, d, 0, dataSize, memory.Nullptr))
			} else {
				write(ctx, cb.GlTexImage3D(target, lvl, GLint(fmt), w, h, d, 0, dataFormat, dataType, memory.Nullptr))
			}
		}
	} else {
		for lvl, levelObject := range t.Levels().All() {
			for layer, img := range levelObject.Layers().All() {
				// NB: Each face of cubemap faces can technically have different format and size.
				fmt, w, h := internalFormat(img.SizedFormat()), img.Width(), img.Height()
				dataFormat, dataType := img.getUnsizedFormatAndType()
				target := target
				if target == GLenum_GL_TEXTURE_CUBE_MAP {
					target = GLenum_GL_TEXTURE_CUBE_MAP_POSITIVE_X + GLenum(layer%6)
				}
				if isCompressed(img) {
					write(ctx, cb.GlCompressedTexImage2D(target, lvl, fmt, w, h, 0, GLsizei(img.Data().Size()), memory.Nullptr))
				} else {
					write(ctx, cb.GlTexImage2D(target, lvl, GLint(fmt), w, h, 0, dataFormat, dataType, memory.Nullptr))
				}
			}
		}
	}

	// Upload the layers one by one
	for lvl, levelObject := range t.Levels().All() {
		for layer, img := range levelObject.Layers().All() {
			fmt, w, h, d := internalFormat(img.SizedFormat()), img.Width(), img.Height(), GLsizei(1)
			dataFormat, dataType := img.getUnsizedFormatAndType()
			dataSize, data := GLsizei(img.Data().Size()), sb.readsSlice(ctx, img.Data())

			if dataSize == 0 {
				continue // The texture layer was not initialized.
			} else if target == GLenum_GL_TEXTURE_BUFFER {
				continue // There should be no images or layers.
			} else if target == GLenum_GL_TEXTURE_EXTERNAL_OES {
				continue // The content is fully specified by the EGLimage
			} else if isArray || is3D {
				if isCompressed(img) {
					write(ctx, cb.GlCompressedTexSubImage3D(target, lvl, 0, 0, layer, w, h, d, fmt, dataSize, data))
				} else {
					write(ctx, cb.GlTexSubImage3D(target, lvl, 0, 0, layer, w, h, d, dataFormat, dataType, data))
				}
			} else {
				target := target
				if target == GLenum_GL_TEXTURE_CUBE_MAP {
					target = GLenum_GL_TEXTURE_CUBE_MAP_POSITIVE_X + GLenum(layer%6)
				}
				if isCompressed(img) {
					write(ctx, cb.GlCompressedTexSubImage2D(target, lvl, 0, 0, w, h, fmt, dataSize, data))
				} else {
					write(ctx, cb.GlTexSubImage2D(target, lvl, 0, 0, w, h, dataFormat, dataType, data))
				}
			}
		}
	}

	defaults := MakeTexture(sb.tmpArena)
	parami := func(name GLenum, value GLint, defaultValue GLint) {
		if value != defaultValue || target == GLenum_GL_TEXTURE_EXTERNAL_OES {
			write(ctx, cb.GlTexParameteri(target, name, value))
		}
	}
	parami(GLenum_GL_TEXTURE_MAG_FILTER, GLint(t.MagFilter()), GLint(defaults.MagFilter()))
	parami(GLenum_GL_TEXTURE_MIN_FILTER, GLint(t.MinFilter()), GLint(defaults.MinFilter()))
	parami(GLenum_GL_TEXTURE_WRAP_S, GLint(t.WrapS()), GLint(defaults.WrapS()))
	parami(GLenum_GL_TEXTURE_WRAP_T, GLint(t.WrapT()), GLint(defaults.WrapT()))
	parami(GLenum_GL_TEXTURE_WRAP_R, GLint(t.WrapR()), GLint(defaults.WrapR()))
	parami(GLenum_GL_TEXTURE_COMPARE_FUNC, GLint(t.CompareFunc()), GLint(defaults.CompareFunc()))
	parami(GLenum_GL_TEXTURE_COMPARE_MODE, GLint(t.CompareMode()), GLint(defaults.CompareMode()))
	parami(GLenum_GL_TEXTURE_BASE_LEVEL, t.BaseLevel(), defaults.BaseLevel())
	parami(GLenum_GL_TEXTURE_MAX_LEVEL, t.MaxLevel(), defaults.MaxLevel())
	parami(GLenum_GL_TEXTURE_SWIZZLE_A, GLint(t.SwizzleA()), GLint(defaults.SwizzleA()))
	parami(GLenum_GL_TEXTURE_SWIZZLE_B, GLint(t.SwizzleB()), GLint(defaults.SwizzleB()))
	parami(GLenum_GL_TEXTURE_SWIZZLE_G, GLint(t.SwizzleG()), GLint(defaults.SwizzleG()))
	parami(GLenum_GL_TEXTURE_SWIZZLE_R, GLint(t.SwizzleR()), GLint(defaults.SwizzleR()))
	parami(GLenum_GL_DEPTH_STENCIL_TEXTURE_MODE, GLint(t.DepthStencilTextureMode()), GLint(defaults.DepthStencilTextureMode()))
	if t.MaxLod() != defaults.MaxLod() {
		write(ctx, cb.GlTexParameterf(target, GLenum_GL_TEXTURE_MAX_LOD, t.MaxLod()))
	}
	if t.MinLod() != defaults.MinLod() {
		write(ctx, cb.GlTexParameterf(target, GLenum_GL_TEXTURE_MIN_LOD, t.MinLod()))
	}
	if !t.BorderColor().EqualTo(0, 0, 0, 0) {
		write(ctx, cb.GlTexParameterfv(target, GLenum_GL_TEXTURE_BORDER_COLOR, sb.readsData(ctx, t.BorderColor())))
	} else if !t.BorderColorI().EqualTo(0, 0, 0, 0) {
		write(ctx, cb.GlTexParameteriv(target, GLenum_GL_TEXTURE_BORDER_COLOR, sb.readsData(ctx, t.BorderColorI())))
	}
	if t.MaxAnisotropy() != 1.0 {
		write(ctx, cb.GlTexParameterf(target, GLenum_GL_TEXTURE_MAX_ANISOTROPY_EXT, GLfloat(t.MaxAnisotropy())))
	}
}

// internalFormat returns the correct format to use for texture data upload calls (e.g. glTexImage2d).
// See image_format.api:GetSizedFormatFromTuple.
func internalFormat(fmt GLenum) GLenum {
	switch fmt {
	case GLenum_GL_LUMINANCE8_ALPHA8_EXT, GLenum_GL_LUMINANCE_ALPHA16F_EXT, GLenum_GL_LUMINANCE_ALPHA32F_EXT:
		return GLenum_GL_LUMINANCE_ALPHA
	case GLenum_GL_LUMINANCE8_EXT, GLenum_GL_LUMINANCE16F_EXT, GLenum_GL_LUMINANCE32F_EXT:
		return GLenum_GL_LUMINANCE
	case GLenum_GL_ALPHA8_EXT, GLenum_GL_ALPHA16F_EXT, GLenum_GL_ALPHA32F_EXT:
		return GLenum_GL_ALPHA
	default:
		return fmt
	}
}

func (sb *stateBuilder) samplerObject(ctx context.Context, s Samplerʳ) {
	write, cb, id := sb.write, sb.cb, s.GetID()

	write(ctx, cb.GlSamplerParameteri(id, GLenum_GL_TEXTURE_COMPARE_FUNC, GLint(s.CompareFunc())))
	write(ctx, cb.GlSamplerParameteri(id, GLenum_GL_TEXTURE_COMPARE_MODE, GLint(s.CompareMode())))
	write(ctx, cb.GlSamplerParameteri(id, GLenum_GL_TEXTURE_MIN_FILTER, GLint(s.MinFilter())))
	write(ctx, cb.GlSamplerParameteri(id, GLenum_GL_TEXTURE_MAG_FILTER, GLint(s.MagFilter())))
	write(ctx, cb.GlSamplerParameterf(id, GLenum_GL_TEXTURE_MIN_LOD, GLfloat(s.MinLod())))
	write(ctx, cb.GlSamplerParameterf(id, GLenum_GL_TEXTURE_MAX_LOD, GLfloat(s.MaxLod())))
	write(ctx, cb.GlSamplerParameteri(id, GLenum_GL_TEXTURE_WRAP_R, GLint(s.WrapR())))
	write(ctx, cb.GlSamplerParameteri(id, GLenum_GL_TEXTURE_WRAP_S, GLint(s.WrapS())))
	write(ctx, cb.GlSamplerParameteri(id, GLenum_GL_TEXTURE_WRAP_T, GLint(s.WrapT())))
	write(ctx, cb.GlSamplerParameterf(id, GLenum_GL_TEXTURE_MAX_ANISOTROPY_EXT, GLfloat(s.MaxAnisotropy())))
	if !s.BorderColor().EqualTo(0, 0, 0, 0) {
		write(ctx, cb.GlSamplerParameterfv(id, GLenum_GL_TEXTURE_BORDER_COLOR, sb.readsData(ctx, s.BorderColor()))) // GLES32
	} else if !s.BorderColorI().EqualTo(0, 0, 0, 0) {
		write(ctx, cb.GlSamplerParameterIiv(id, GLenum_GL_TEXTURE_BORDER_COLOR, sb.readsData(ctx, s.BorderColorI()))) // GLES32
	}
}

func (sb *stateBuilder) queryObject(ctx context.Context, q Queryʳ) {
	write, cb, id := sb.write, sb.cb, q.GetID()

	target := q.Type()
	write(ctx, cb.GlBeginQuery(target, id))
	if !q.Active() {
		write(ctx, cb.GlEndQuery(target))
	}
}

func (sb *stateBuilder) syncObject(ctx context.Context, s SyncObjectʳ) {
	write, cb := sb.write, sb.cb

	write(ctx, cb.GlFenceSync(GLenum_GL_SYNC_GPU_COMMANDS_COMPLETE, 0, s.ID()))
}

func (sb *stateBuilder) transformFeedbackObject(ctx context.Context, tf TransformFeedbackʳ) {
	write, cb, id := sb.write, sb.cb, tf.GetID()

	write(ctx, cb.GlBindTransformFeedback(GLenum_GL_TRANSFORM_FEEDBACK, id))
	if tf.Active() == GLboolean_GL_TRUE {
		sb.E(ctx, "Transform feedback data was not restored") // TODO
		write(ctx, cb.GlBeginTransformFeedback(tf.PrimitiveMode()))
		write(ctx, cb.GlPauseTransformFeedback())
	}
	write(ctx, cb.GlBindTransformFeedback(GLenum_GL_TRANSFORM_FEEDBACK, 0))
}

func (sb *stateBuilder) vertexArrayObject(ctx context.Context, vao VertexArrayʳ) {
	write, cb, id := sb.write, sb.cb, vao.GetID()

	write(ctx, cb.GlBindVertexArray(id))
	if id > 0 {
		for loc, vaa := range vao.VertexAttributeArrays().All() {
			defaultArray := MakeVertexAttributeArray(sb.tmpArena)
			defaultArray.SetBinding(vao.VertexBufferBindings().Get(VertexBufferBindingIndex(loc)))
			if vaa.Get().Equals(defaultArray) {
				continue // Default
			}
			if vaa.Enabled() == GLboolean_GL_TRUE {
				write(ctx, cb.GlEnableVertexAttribArray(loc))
			}
			if vaa.Stride() != 0 || vaa.Pointer() != memory.Nullptr {
				// Same as glVertexAttribFormat for our purposes here, with the difference that
				// it sets the obsolete and unused properties 'stride' and 'pointer'.
				write(ctx, cb.GlBindBuffer(GLenum_GL_ARRAY_BUFFER, vaa.Binding().Buffer().GetID()))
				if vaa.Integer() == GLboolean_GL_TRUE {
					write(ctx, cb.GlVertexAttribIPointer(loc, vaa.Size(), vaa.Type(), vaa.Stride(), vaa.Pointer()))
				} else {
					write(ctx, cb.GlVertexAttribPointer(loc, vaa.Size(), vaa.Type(), vaa.Normalized(), vaa.Stride(), vaa.Pointer()))
				}
			} else {
				if vaa.Integer() == GLboolean_GL_TRUE {
					write(ctx, cb.GlVertexAttribIFormat(loc, vaa.Size(), vaa.Type(), vaa.RelativeOffset()))
				} else {
					write(ctx, cb.GlVertexAttribFormat(loc, vaa.Size(), vaa.Type(), vaa.Normalized(), vaa.RelativeOffset()))
				}
			}
			if VertexBufferBindingIndex(loc) != vaa.Binding().Id() {
				write(ctx, cb.GlVertexAttribBinding(loc, vaa.Binding().Id()))
			}
		}
		for i, b := range vao.VertexBufferBindings().All() {
			defaultBinding := MakeVertexBufferBinding(sb.tmpArena)
			defaultBinding.SetId(VertexBufferBindingIndex(i))
			if b.Get().Equals(defaultBinding) {
				continue
			}
			write(ctx, cb.GlBindVertexBuffer(i, b.Buffer().GetID(), b.Offset(), b.Stride()))
			if divisor := b.Divisor(); divisor != 0 {
				write(ctx, cb.GlVertexBindingDivisor(i, divisor))
			}
		}
	} else {
		for loc, vaa := range vao.VertexAttributeArrays().All() {
			defaultArray := MakeVertexAttributeArray(sb.tmpArena)
			defaultArray.SetBinding(vao.VertexBufferBindings().Get(VertexBufferBindingIndex(loc)))
			if vaa.Get().Equals(defaultArray) {
				continue // Default
			}
			if vaa.Enabled() == GLboolean_GL_TRUE {
				write(ctx, cb.GlEnableVertexAttribArray(loc))
			}
			write(ctx, cb.GlBindBuffer(GLenum_GL_ARRAY_BUFFER, vaa.Binding().Buffer().GetID()))
			if vaa.Integer() == GLboolean_GL_TRUE {
				write(ctx, cb.GlVertexAttribIPointer(loc, vaa.Size(), vaa.Type(), vaa.Stride(), vaa.Pointer()))
			} else {
				write(ctx, cb.GlVertexAttribPointer(loc, vaa.Size(), vaa.Type(), vaa.Normalized(), vaa.Stride(), vaa.Pointer()))
			}
			if divisor := vaa.Binding().Divisor(); divisor != 0 {
				write(ctx, cb.GlVertexAttribDivisor(loc, divisor))
			}
		}
	}
	write(ctx, cb.GlBindBuffer(GLenum_GL_ARRAY_BUFFER, 0))
	write(ctx, cb.GlBindVertexArray(0))
}

func (sb *stateBuilder) vertexState(ctx context.Context, vs VertexState) {
	write, cb := sb.write, sb.cb

	for loc, att := range vs.Attributes().All() {
		switch att.Type() {
		case GLenum_GL_FLOAT_VEC4:
			write(ctx, cb.GlVertexAttrib4fv(loc, sb.readsSlice(ctx, att.Value())))
		case GLenum_GL_INT_VEC4:
			write(ctx, cb.GlVertexAttribI4iv(loc, sb.readsSlice(ctx, att.Value())))
		case GLenum_GL_UNSIGNED_INT_VEC4:
			write(ctx, cb.GlVertexAttribI4uiv(loc, sb.readsSlice(ctx, att.Value())))
		}
	}

	write(ctx, cb.GlPatchParameteri(GLenum_GL_PATCH_VERTICES, vs.PatchVertices()))

	sb.enable(ctx, GLenum_GL_PRIMITIVE_RESTART_FIXED_INDEX, vs.PrimitiveRestartFixedIndex())
}

func (sb *stateBuilder) reasterizationState(ctx context.Context, rs RasterizationState) {
	write, cb := sb.write, sb.cb

	write(ctx, cb.GlViewport(rs.Viewport().X(), rs.Viewport().Y(), rs.Viewport().Width(), rs.Viewport().Height()))
	write(ctx, cb.GlDepthRangef(rs.DepthRange().Get(0), rs.DepthRange().Get(1)))
	if !rs.PrimitiveBoundingBox().IsUnit() {
		min, max := rs.PrimitiveBoundingBox().Min(), rs.PrimitiveBoundingBox().Max()
		write(ctx, cb.GlPrimitiveBoundingBox(
			min.Get(0), min.Get(1), min.Get(2), min.Get(3),
			max.Get(0), max.Get(1), max.Get(2), max.Get(3)),
		)
	}

	//  Rasterization
	sb.enable(ctx, GLenum_GL_RASTERIZER_DISCARD, rs.RasterizerDiscard())
	write(ctx, cb.GlLineWidth(rs.LineWidth()))
	sb.enable(ctx, GLenum_GL_CULL_FACE, rs.CullFace())
	write(ctx, cb.GlCullFace(rs.CullFaceMode()))
	write(ctx, cb.GlFrontFace(rs.FrontFace()))
	write(ctx, cb.GlPolygonOffset(rs.PolygonOffsetFactor(), rs.PolygonOffsetUnits()))
	sb.enable(ctx, GLenum_GL_POLYGON_OFFSET_FILL, rs.PolygonOffsetFill())

	// Multisampling
	sb.enable(ctx, GLenum_GL_SAMPLE_ALPHA_TO_COVERAGE, rs.SampleAlphaToCoverage())
	sb.enable(ctx, GLenum_GL_SAMPLE_COVERAGE, rs.SampleCoverage())
	write(ctx, cb.GlSampleCoverage(rs.SampleCoverageValue(), rs.SampleCoverageInvert()))
	sb.enable(ctx, GLenum_GL_SAMPLE_SHADING, rs.SampleShading())
	write(ctx, cb.GlMinSampleShading(rs.MinSampleShadingValue()))
	sb.enable(ctx, GLenum_GL_SAMPLE_MASK, rs.SampleMask())
	for i, mask := range rs.SampleMaskValue().All() {
		write(ctx, cb.GlSampleMaski(i, mask)) // GLES31
	}
}

func (sb *stateBuilder) pixelState(ctx context.Context, ps PixelState) {
	write, cb := sb.write, sb.cb

	// Scissor
	sb.enable(ctx, GLenum_GL_SCISSOR_TEST, ps.Scissor().Test())
	write(ctx, cb.GlScissor(ps.Scissor().Box().X(), ps.Scissor().Box().Y(), ps.Scissor().Box().Width(), ps.Scissor().Box().Height()))

	// Stencil
	st := ps.Stencil()
	sb.enable(ctx, GLenum_GL_STENCIL_TEST, st.Test())
	write(ctx, cb.GlStencilFuncSeparate(GLenum_GL_FRONT, st.Func(), st.Ref(), st.ValueMask()))
	write(ctx, cb.GlStencilOpSeparate(GLenum_GL_FRONT, st.Fail(), st.PassDepthFail(), st.PassDepthPass()))
	write(ctx, cb.GlStencilFuncSeparate(GLenum_GL_BACK, st.BackFunc(), st.BackRef(), st.BackValueMask()))
	write(ctx, cb.GlStencilOpSeparate(GLenum_GL_BACK, st.BackFail(), st.BackPassDepthFail(), st.BackPassDepthPass()))

	// Blend
	write(ctx, cb.GlBlendColor(ps.BlendColor().Red(), ps.BlendColor().Green(), ps.BlendColor().Blue(), ps.BlendColor().Alpha()))
	for i, bs := range ps.Blend().All() {
		write(ctx, cb.GlBlendEquationSeparatei(i, bs.EquationRgb(), bs.EquationAlpha()))               // GLES32
		write(ctx, cb.GlBlendFuncSeparatei(i, bs.SrcRgb(), bs.DstRgb(), bs.SrcAlpha(), bs.DstAlpha())) // GLES32
		sb.enablei(ctx, GLenum_GL_BLEND, GLuint(i), bs.Enabled())                                      // GLES32?
	}

	// Depth
	sb.enable(ctx, GLenum_GL_DEPTH_TEST, ps.Depth().Test())
	write(ctx, cb.GlDepthFunc(ps.Depth().Func()))

	sb.enable(ctx, GLenum_GL_DITHER, ps.Dither())
	sb.enable(ctx, GLenum_GL_FRAMEBUFFER_SRGB_EXT, ps.FramebufferSrgb())

	// Framebuffer control
	for i, mask := range ps.ColorWritemask().All() {
		write(ctx, cb.GlColorMaski(i, mask.R(), mask.G(), mask.B(), mask.A())) // GLES32
	}
	write(ctx, cb.GlDepthMask(ps.DepthWritemask()))
	write(ctx, cb.GlStencilMaskSeparate(GLenum_GL_FRONT, ps.StencilWritemask()))
	write(ctx, cb.GlStencilMaskSeparate(GLenum_GL_BACK, ps.StencilBackWritemask()))
	clearColor := ps.ColorClearValue()
	write(ctx, cb.GlClearColor(clearColor.Get(0), clearColor.Get(1), clearColor.Get(2), clearColor.Get(3)))
	write(ctx, cb.GlClearDepthf(ps.DepthClearValue()))
	write(ctx, cb.GlClearStencil(ps.StencilClearValue()))
}

// must be done after all data uploads (because it sets unpack state)
func (sb *stateBuilder) otherState(ctx context.Context, os OtherState) {
	write, cb := sb.write, sb.cb

	// glPixelStorei
	write(ctx, cb.GlPixelStorei(GLenum_GL_PACK_ALIGNMENT, os.Pack().Alignment()))
	write(ctx, cb.GlPixelStorei(GLenum_GL_PACK_IMAGE_HEIGHT, os.Pack().ImageHeight()))
	write(ctx, cb.GlPixelStorei(GLenum_GL_PACK_ROW_LENGTH, os.Pack().RowLength()))
	write(ctx, cb.GlPixelStorei(GLenum_GL_PACK_SKIP_IMAGES, os.Pack().SkipImages()))
	write(ctx, cb.GlPixelStorei(GLenum_GL_PACK_SKIP_PIXELS, os.Pack().SkipPixels()))
	write(ctx, cb.GlPixelStorei(GLenum_GL_PACK_SKIP_ROWS, os.Pack().SkipRows()))
	write(ctx, cb.GlPixelStorei(GLenum_GL_UNPACK_ALIGNMENT, os.Unpack().Alignment()))
	write(ctx, cb.GlPixelStorei(GLenum_GL_UNPACK_IMAGE_HEIGHT, os.Unpack().ImageHeight()))
	write(ctx, cb.GlPixelStorei(GLenum_GL_UNPACK_ROW_LENGTH, os.Unpack().RowLength()))
	write(ctx, cb.GlPixelStorei(GLenum_GL_UNPACK_SKIP_IMAGES, os.Unpack().SkipImages()))
	write(ctx, cb.GlPixelStorei(GLenum_GL_UNPACK_SKIP_PIXELS, os.Unpack().SkipPixels()))
	write(ctx, cb.GlPixelStorei(GLenum_GL_UNPACK_SKIP_ROWS, os.Unpack().SkipRows()))

	// Debug state
	write(ctx, cb.GlDebugMessageCallback(os.Debug().CallbackFunction(), os.Debug().CallbackUserParam()))
	sb.enable(ctx, GLenum_GL_DEBUG_OUTPUT, os.Debug().Output())
	sb.enable(ctx, GLenum_GL_DEBUG_OUTPUT_SYNCHRONOUS, os.Debug().OutputSynchronous())
}

func (sb *stateBuilder) debugLabels(ctx context.Context, c Contextʳ) {
	write, cb := sb.write, sb.cb

	label := func(target GLenum, id GLuint, text string) {
		if text != "" {
			write(ctx, cb.GlObjectLabel(target, id, GLsizei(len(text)), sb.readsData(ctx, text)))
		}
	}
	for id, obj := range c.Objects().Textures().All() {
		label(GLenum_GL_TEXTURE, GLuint(id), obj.Label())
	}
	for id, obj := range c.Objects().Framebuffers().All() {
		label(GLenum_GL_FRAMEBUFFER, GLuint(id), obj.Label())
	}
	for id, obj := range c.Objects().Renderbuffers().All() {
		label(GLenum_GL_RENDERBUFFER, GLuint(id), obj.Label())
	}
	for id, obj := range c.Objects().Buffers().All() {
		label(GLenum_GL_BUFFER, GLuint(id), obj.Label())
	}
	for id, obj := range c.Objects().Shaders().All() {
		label(GLenum_GL_SHADER, GLuint(id), obj.Label())
	}
	for id, obj := range c.Objects().Programs().All() {
		label(GLenum_GL_PROGRAM, GLuint(id), obj.Label())
	}
	for id, obj := range c.Objects().VertexArrays().All() {
		label(GLenum_GL_VERTEX_ARRAY, GLuint(id), obj.Label())
	}
	for id, obj := range c.Objects().Queries().All() {
		label(GLenum_GL_QUERY, GLuint(id), obj.Label())
	}
	for id, obj := range c.Objects().Samplers().All() {
		label(GLenum_GL_SAMPLER, GLuint(id), obj.Label())
	}
	for id, obj := range c.Objects().TransformFeedbacks().All() {
		label(GLenum_GL_TRANSFORM_FEEDBACK, GLuint(id), obj.Label())
	}
	for id, obj := range c.Objects().Pipelines().All() {
		label(GLenum_GL_PROGRAM_PIPELINE, GLuint(id), obj.Label())
	}
}

// bindings must be done at the end, since other stages overwrite them.
func (sb *stateBuilder) bindings(ctx context.Context, c Contextʳ) {
	write, cb := sb.write, sb.cb

	// Most of this method assumes binding points are initialized to 0.
	// So first reset all binding points which used in other code to 0.
	write(ctx, cb.GlBindBuffer(GLenum_GL_ARRAY_BUFFER, 0))
	write(ctx, cb.GlBindRenderbuffer(GLenum_GL_RENDERBUFFER, 0))
	write(ctx, cb.GlBindFramebuffer(GLenum_GL_FRAMEBUFFER, 0))

	// TODO: This does not handle deleted objects which are still bound.

	// Indexed-buffers
	{
		bind := func(target GLenum, bindings GLuintːBufferBindingᵐ) {
			for index, b := range bindings.All() {
				if id := b.Binding().GetID(); id != 0 {
					write(ctx, cb.GlBindBufferRange(target, index, id, b.Start(), b.Size()))
				}
			}
		}
		bind(GLenum_GL_UNIFORM_BUFFER, c.Bound().UniformBuffers())              //GLES30
		bind(GLenum_GL_ATOMIC_COUNTER_BUFFER, c.Bound().AtomicCounterBuffers()) //GLES31
		bind(GLenum_GL_SHADER_STORAGE_BUFFER, c.Bound().ShaderStorageBuffers()) //GLES31
		for _, tf := range c.Objects().TransformFeedbacks().All() {
			write(ctx, cb.GlBindTransformFeedback(GLenum_GL_TRANSFORM_FEEDBACK, tf.GetID()))
			bind(GLenum_GL_TRANSFORM_FEEDBACK_BUFFER, tf.Buffers()) //GLES30
		}
	}

	// Non-indexed buffers (must be after indexed due to generic binding points)
	{
		bind := func(target GLenum, b Bufferʳ) {
			if id := b.GetID(); id != 0 {
				write(ctx, cb.GlBindBuffer(target, id))
			}
		}
		bind(GLenum_GL_ARRAY_BUFFER, c.Bound().ArrayBuffer())                              //GLES20
		bind(GLenum_GL_ELEMENT_ARRAY_BUFFER, c.Bound().VertexArray().ElementArrayBuffer()) //GLES20
		bind(GLenum_GL_COPY_READ_BUFFER, c.Bound().CopyReadBuffer())                       //GLES30
		bind(GLenum_GL_COPY_WRITE_BUFFER, c.Bound().CopyWriteBuffer())                     //GLES30
		bind(GLenum_GL_PIXEL_PACK_BUFFER, c.Bound().PixelPackBuffer())                     //GLES30
		bind(GLenum_GL_PIXEL_UNPACK_BUFFER, c.Bound().PixelUnpackBuffer())                 //GLES30
		bind(GLenum_GL_UNIFORM_BUFFER, c.Bound().UniformBuffer())                          //GLES30
		bind(GLenum_GL_ATOMIC_COUNTER_BUFFER, c.Bound().AtomicCounterBuffer())             //GLES31
		bind(GLenum_GL_DISPATCH_INDIRECT_BUFFER, c.Bound().DispatchIndirectBuffer())       //GLES31
		bind(GLenum_GL_DRAW_INDIRECT_BUFFER, c.Bound().DrawIndirectBuffer())               //GLES31
		bind(GLenum_GL_SHADER_STORAGE_BUFFER, c.Bound().ShaderStorageBuffer())             //GLES31
		bind(GLenum_GL_TEXTURE_BUFFER, c.Bound().TextureBuffer())                          //GLES32
		for _, tf := range c.Objects().TransformFeedbacks().All() {
			write(ctx, cb.GlBindTransformFeedback(GLenum_GL_TRANSFORM_FEEDBACK, tf.GetID()))
			bind(GLenum_GL_TRANSFORM_FEEDBACK_BUFFER, tf.Buffer()) //GLES30
		}
	}

	// Samplers
	for unit, tu := range c.Objects().TextureUnits().All() {
		if sampler := tu.SamplerBinding().GetID(); sampler != 0 {
			write(ctx, cb.GlBindSampler(GLuint(unit), sampler))
		}
	}

	// Programs
	write(ctx, cb.GlUseProgram(c.Bound().Program().GetID()))
	if id := c.Bound().Pipeline().GetID(); id != 0 {
		write(ctx, cb.GlBindProgramPipeline(id))
	}

	// Transform feedback
	write(ctx, cb.GlBindTransformFeedback(GLenum_GL_TRANSFORM_FEEDBACK, c.Bound().TransformFeedback().GetID()))
	if c.Bound().TransformFeedback().Active() == GLboolean_GL_TRUE &&
		c.Bound().TransformFeedback().Paused() == GLboolean_GL_FALSE {
		// Active and unpaused transform feedback is very limiting on possible state modifications.
		// So create it in paused state and resume it fairly late (only one can be active&resumed).
		write(ctx, cb.GlResumeTransformFeedback())
	}

	// Vertex Array
	write(ctx, cb.GlBindVertexArray(c.Bound().VertexArray().GetID()))
}

// deleteMarkedObjects will delete objects if they are marked for deletion.
// This must be done after bindings since bindings are still keeping them alive.
func (sb *stateBuilder) deleteMarkedObjects(ctx context.Context, c Contextʳ) {
	write, cb := sb.write, sb.cb

	for id, s := range c.Objects().Shaders().All() {
		if s.DeleteStatus() {
			write(ctx, cb.GlDeleteShader(id))
		}
	}
	for id, p := range c.Objects().Programs().All() {
		if p.DeleteStatus() {
			write(ctx, cb.GlDeleteProgram(id))
		}
	}
}
